package icasue.base.utils.wareable;

import icasue.base.utils.annotations.ConvertField;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.springframework.lang.Nullable;

import javax.validation.constraints.NotNull;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.*;
import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Consumer;

/**
 * @Author: Qiao Hang
 * @CreateDate: 2020/7/15 下午4:46
 * @UpdateDate:
 * @Description:
 */
@Slf4j
public class FieldConvertUtil {

    private static volatile Map<String,List<HandleTransRecorder>> cacheConvertHandle = new ConcurrentHashMap<>();
    private static final int ALLOWED_MODES = MethodHandles.Lookup.PRIVATE | MethodHandles.Lookup.PROTECTED
            | MethodHandles.Lookup.PACKAGE | MethodHandles.Lookup.PUBLIC;
    public static final MethodHandles.Lookup methodLookup;

    static {
        //get lookup
        MethodHandles.Lookup lookupTemp = null;
        try {
            //java 9
            Method lookupMethod = MethodHandles.class.getDeclaredMethod("privateLookupIn", Class.class, MethodHandles.Lookup.class);
            lookupMethod.setAccessible(true);
            MethodHandles.Lookup lookup =
                    (MethodHandles.Lookup)lookupMethod.invoke(null, MethodHandles.Lookup.class, MethodHandles.lookup());
            lookupTemp = lookup;
        }catch (Exception e){
            //java 8
            try {
                Constructor<MethodHandles.Lookup> lookupConstruct = MethodHandles.Lookup.class.getDeclaredConstructor(Class.class, int.class);
                lookupConstruct.setAccessible(true);
                lookupTemp = lookupConstruct.newInstance(MethodHandles.Lookup.class, ALLOWED_MODES);
            } catch (Exception e1) {
                throw new RuntimeException(e1.getCause());
            }
        }
        methodLookup = lookupTemp;
    }

    /**
     * This method support common convert logic, from source to targetClass,
     * return a target instance ,which field already be set through @ConvertField defined.
     * notice: simpleConvert only set fields which has @ConvertField annotation,and JavaType is convertAble.
     * @param source source data
     * @param targetClass target class
     * @param area  convert area,which ensure in diff service or request.
     * @param preDealLogic  source's pre deal logic
     * @param postDealLogic target's post deal logic
     * @param <S>
     * @param <T>
     * @return
     */
    public static <S,T> T simpleConvert(S source, Class<? extends T> targetClass, String area,Consumer<S> preDealLogic,Consumer<T> postDealLogic){
        List<HandleTransRecorder> handleTransRecorders =
                FieldConvertUtil.getSetCacheMethodHandle(source.getClass(), targetClass, area);
        try {
            T target = FieldConvertUtil.instanceTarget(targetClass);
            FieldConvertUtil.doInternalProcessor( handleTransRecorders,
                    source,target,
                    preDealLogic,postDealLogic,
                    "FieldConvertUtil.simpleConvert"
            );
            return target;
        } catch (Throwable e) {
            if(e instanceof FiledConvertError){
                throw (FiledConvertError)e;
            }
            throw FiledConvertError.build(e,null,"Instance target error,check constructor pls.");
        }
    }

    /**
     * This method support common convert logic, from source to targetClass,
     * return a target instance ,which field already be set through @ConvertField defined.
     * notice: simpleConvert only set fields which has @ConvertField annotation,and JavaType is convertAble.
     * @param source source data
     * @param targetPre target data already be instance.
     * @param area  convert area,which ensure in diff service or request.
     * @param preDealLogic  source's pre deal logic
     * @param postDealLogic target's post deal logic
     * @param <S>
     * @param <T>
     * @return
     */
    public static <S,T> T simpleConvertDirect(S source, @NonNull T targetPre, String area, Consumer<S> preDealLogic, Consumer<T> postDealLogic){
        List<HandleTransRecorder> handleTransRecorders =
                FieldConvertUtil.getSetCacheMethodHandle(source.getClass(), targetPre.getClass(), area);
        try {
            FieldConvertUtil.doInternalProcessor( handleTransRecorders,
                    source,targetPre,
                    preDealLogic,postDealLogic,
                    "FieldConvertUtil.simpleConvertDirect"
            );
            return targetPre;
        } catch (Throwable e) {
            if(e instanceof FiledConvertError){
                throw (FiledConvertError)e;
            }
            throw FiledConvertError.build(e,null,"");
        }
    }

    /**
     * Instance target, include case when target class exist as a inner class.
     * @param targetClass
     * @param <T>
     * @return
     * @throws Throwable
     */
    private static <T> T instanceTarget(Class<T> targetClass) throws Throwable {
        T target = null;
        if(targetClass.isMemberClass() && !Modifier.isStatic(targetClass.getModifiers())){
            target = BeanCopierUtil.tryInstanceDependOnOuterClass(targetClass, new ArrayList<>());
        }else {
            target = targetClass.getDeclaredConstructor().newInstance();
        }
        return target;
    }

    /**
     * This method's defined for common convert logic.
     * @param handleTransRecorders Getter and Setter's mapping.
     * @param source source instance.
     * @param target target instance.
     * @param preDealLogic previous convert extra logic.
     * @param postDealLogic after convert extra logic.
     * @param executeMethod Method marked which error occurred.
     * @param <S>
     * @param <T>
     * @return target instance after drawing.
     * @throws Throwable
     */
    private static <S,T> T doInternalProcessor(List<HandleTransRecorder> handleTransRecorders,
                                               S source, @NotNull T target,
                                               Consumer<S> preDealLogic,Consumer<T> postDealLogic,
                                               @NotNull String executeMethod) throws RuntimeException {
        try {
            //execute previous extra logic.
            if(preDealLogic != null){
                preDealLogic.accept(source);
            }
        }catch (Throwable e1){
            throw FiledConvertError.build(
                    e1,FiledConvertError.ErrorPosition.PREVIOUS,
                    String.join(executeMethod," : ", "execute previous logic occur an exception.")
            );
        }
        try {
            //execute get set handle.
            HandleTransRecorder.simpleGetSetHandle(handleTransRecorders,source,target);
        }catch (Throwable e2){
            throw FiledConvertError.build(
                    e2,FiledConvertError.ErrorPosition.CONVERT,
                    String.join(executeMethod," : ", "execute convert occur an exception.")
            );
        }
        try {
            //execute post extra logic.
            if(postDealLogic != null){
                postDealLogic.accept(target);
            }
        }catch (Throwable e3){
            throw FiledConvertError.build(
                    e3,FiledConvertError.ErrorPosition.CONVERT,
                    String.join(executeMethod," : ", "execute post logic occur an exception.")
            );
        }
        return target;
    }



    /**
     * This method is used for find source class and target class's Getter and Setter Method's handle,
     * and MethodHandle is cacheAble, MethodHandle's parch logic is defined in @ConvertField annotations and area.
     * @param sourceClass
     * @param targetClass
     * @param area
     * @return
     */
    private static List<HandleTransRecorder> getSetCacheMethodHandle(Class<?> sourceClass,Class<?> targetClass,String area){
        String cacheClassConcat = sourceClass.getCanonicalName().concat("_")
                .concat(area).concat("_").concat(targetClass.getCanonicalName());
        List<HandleTransRecorder> handleTransRecorders = cacheConvertHandle.get(cacheClassConcat);
        if(handleTransRecorders != null){
            return handleTransRecorders;
        }
        //Construct row handler tarns recorder.
        List<HandleTransRecorder> rowHandleTransRecorderList = new CopyOnWriteArrayList<>();
        //get mapping logic.
        Set<Field> waitingCopierFields = new HashSet<>();
        waitingCopierFields.addAll(Arrays.asList(sourceClass.getFields()));
        waitingCopierFields.addAll(Arrays.asList(sourceClass.getDeclaredFields()));

        waitingCopierFields.stream()
                .forEach((field) -> {
                    ConvertField[] annotationsByType = field.getAnnotationsByType(ConvertField.class);
                    Optional<ConvertField> any = Arrays.stream(annotationsByType)
                            .filter((anAreaConvert) -> anAreaConvert.area().equals(area))
                            .findAny();
                    if(any.isPresent()){
                        //get SourceField's GetterMethodHandle and TargetField's SetterHandle.
                        field.setAccessible(true);
                        MethodHandle sourceFieldGetterMethodHandle = null;
                        try {
                            sourceFieldGetterMethodHandle = methodLookup.unreflectGetter(field);
                            ConvertField suitConvertField = any.get();
                            String targetFieldName = suitConvertField.fieldName();
                            Field mappingTargetFiled = null;
                            try {
                                mappingTargetFiled = targetClass.getDeclaredField(targetFieldName);
                            }catch (Throwable e){
                                try {
                                    mappingTargetFiled = targetClass.getField(targetFieldName);
                                }catch (Throwable e1){
                                    log.info("There is no field name: {} , in class : {}, this fields copier will be skip.",
                                            targetFieldName, targetClass.getCanonicalName());
                                }
                            }
                            if(mappingTargetFiled != null){
                                //Setter's handle get.
                                mappingTargetFiled.setAccessible(true);
                                MethodHandle targetFieldSetterMethodHandle = methodLookup.unreflectSetter(mappingTargetFiled);
                                //create getter setter handles mapping unit.retain source's native Getter and target's native Setter.
                                HandleTransRecorder rowHandleTransRecorder = new HandleTransRecorder();
                                rowHandleTransRecorder.setSourceGetter(methodLookup.unreflectGetter(field));
                                rowHandleTransRecorder.setTargetSetter(targetFieldSetterMethodHandle);
                                //consider if Setter and Getter's JavaType if suitable or not? if not,try change MethodHandle.
                                if(!field.getType().equals(mappingTargetFiled.getType())){
                                    //this flag is used for control filling MethodHandle's step if continue,
                                    // used for origin MethodHandle or new MethodHanlde which after JavaType trans.
                                    boolean continueStep = true;
                                    //step one,filter if Getter's result to string.
                                    MethodHandle getterResultToStringHandle = null;
                                    if(continueStep && (getterResultToStringHandle =
                                            methodLookup.findVirtual(field.getType(), "toString", MethodType.methodType(String.class))) != null){
                                        getterResultToStringHandle = MethodHandles.filterReturnValue(sourceFieldGetterMethodHandle,getterResultToStringHandle);
                                    }else { continueStep = false; }
                                    //step two,filter getter's value as string to Setter's needed JavaType.
                                    MethodHandle supportStringValueToSetterTypeHandle = null;
                                    if(continueStep && (supportStringValueToSetterTypeHandle =
                                            TransSupportJavaType.findStringConvertHandle(mappingTargetFiled.getType())) != null){
                                        if(TransSupportJavaType.STRING.instanceMethodHandle != supportStringValueToSetterTypeHandle) {
                                            supportStringValueToSetterTypeHandle = MethodHandles.filterReturnValue(getterResultToStringHandle, supportStringValueToSetterTypeHandle);
                                        }else {
                                            supportStringValueToSetterTypeHandle = getterResultToStringHandle;
                                        }
                                    }else { continueStep = false; }
                                    //step three,check if or not Setter's needed handle find successfully,reset mapping SetterHandle.
                                    if(continueStep){
                                        //filling MethodHandle success, reset Getter's Handle as a handle chain.
                                        sourceFieldGetterMethodHandle = supportStringValueToSetterTypeHandle;
                                    }
                                    rowHandleTransRecorder.transSuccess(sourceFieldGetterMethodHandle);
                                }
                                rowHandleTransRecorderList.add(rowHandleTransRecorder);
                            }
                        } catch (Throwable e) {
                            throw new RuntimeException("cacheMethodHandle getSet occur an exception. ");
                        }
                    }
                 });
        cacheConvertHandle.put(cacheClassConcat, rowHandleTransRecorderList);
        return rowHandleTransRecorderList;
    }


    /**
     * This class is used for recording trans details about source instance's Getter and target instance's Setter
     */
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public static final class HandleTransRecorder{
        //This file is marked if digest Getter and Setter successfully.
        Boolean transSuccess = Boolean.FALSE;
        Field sourceFiled;
        Field targetFiled;
        MethodHandle sourceGetter;
        MethodHandle targetSetter;
        MethodHandle sourceGetterToTargetSetter;

        void transSuccess(MethodHandle sourceGetterToTargetSetter){
            this.sourceGetterToTargetSetter = sourceGetterToTargetSetter;
            this.transSuccess = Boolean.TRUE;
        }

        public <S, T> void doGetSet(S source, T target) throws Throwable {
            Object nativeValue = this.sourceGetter.bindTo(source).invoke();
            if(nativeValue != null) {
                if(this.transSuccess.equals(Boolean.TRUE)){
                    //Maybe need trans Getter's result to Setter's parameter.
                    //Setter handle's paramType[0]'s description is NativeClass,paramType[1] for SetterMethodHandle description is realParam's javaType
                    Class<?> setterHandleParamClass = targetSetter.type().parameterType(1);
                    if (setterHandleParamClass.isAssignableFrom(nativeValue.getClass())) {
                        targetSetter.bindTo(target).invoke(nativeValue);
                    } else {
                        Object nullAbleTransForParamValueIn = TransSupportJavaType.tryConstructSupportTransObject(setterHandleParamClass, nativeValue);
                        targetSetter.bindTo(target).invoke(nullAbleTransForParamValueIn);
                    }
                }else {
                    targetSetter.bindTo(target).invoke(nativeValue);
                }
            }
        }

        public static <S, T> void simpleGetSetHandle(List<HandleTransRecorder> handleTransRecorders,S source, T target) throws Throwable {
            for (HandleTransRecorder handleTransRecorder : handleTransRecorders) {
                handleTransRecorder.doGetSet(source,target);
            }
        }
    }


    /**
     * FieldConvertUtil execute error cause.
     */
    @Data
    public static class FiledConvertError extends RuntimeException {
        String warning;
        ErrorPosition position;
        @Nullable
        Throwable nativeError;

        public static class AfterLogicExecuteError extends FiledConvertError{ }
        public static class ConvertLogicExecuteError extends FiledConvertError{ }
        public static class PreviousLogicExecuteError extends FiledConvertError{ }

        public static RuntimeException build(Throwable nativeError,ErrorPosition position,String warning){
            RuntimeException throwing = null;
            if(position == null){
                throwing = nativeError == null ? new RuntimeException(warning)
                        : new RuntimeException(warning,nativeError.getCause());
            }else {
                for (ErrorPosition errorPosition : ErrorPosition.values()) {
                    if(errorPosition.equals(position)){
                        try {
                            FiledConvertError filedConvertError = errorPosition.type.newInstance();
                            filedConvertError.warning = warning;
                            filedConvertError.nativeError = nativeError;
                            throwing = filedConvertError;
                        } catch (Throwable e) { }
                    }
                }
            }
            return throwing;
        }

        public static RuntimeException build(ErrorPosition position,String warning){
            return FiledConvertError.build(null,position,warning);
        }

        protected enum ErrorPosition{
            PREVIOUS(PreviousLogicExecuteError.class),
            CONVERT(ConvertLogicExecuteError.class),
            AFTER(AfterLogicExecuteError.class);
            Class<? extends FiledConvertError> type;
            ErrorPosition(Class<? extends FiledConvertError> type){
                this.type = type;
            }
        }

    }

    /**
     * TransSupportJavaType is defined for some Normal JavaType's string construct MethodHandle.
     */
    public enum TransSupportJavaType{
        INTEGER(Integer.class),
        STRING(String.class),
        LONG(Long.class),
        DOUBLE(Double.class),
        BIG_DECIMAL(BigDecimal.class),
        FLOAT(Float.class),
        BOOLEAN(Boolean.class),
        SHORT(Short.class),
        BYTE(Byte.class);
        public Class<?> assignClass;
        public MethodHandle instanceMethodHandle;
        TransSupportJavaType(Class<?> assignClass){
            this.assignClass = assignClass;
            try {
                if(assignClass == Boolean.class){
                    this.instanceMethodHandle = methodLookup.findStatic(Boolean.class,"valueOf", MethodType.methodType(Boolean.class,String.class));
                }else if(assignClass == Byte.class){
                    this.instanceMethodHandle = methodLookup.findStatic(Byte.class,"valueOf", MethodType.methodType(Byte.class,String.class));
                }else if(assignClass == Integer.class){
                    this.instanceMethodHandle = methodLookup.findStatic(Integer.class,"valueOf", MethodType.methodType(Integer.class,String.class));
                }else if(assignClass == Float.class){
                    this.instanceMethodHandle = methodLookup.findStatic(Float.class,"valueOf", MethodType.methodType(Float.class,String.class));
                }else if(assignClass == Double.class){
                    this.instanceMethodHandle = methodLookup.findStatic(Double.class,"valueOf", MethodType.methodType(Double.class,String.class));
                }else if(assignClass == String.class){
                    this.instanceMethodHandle = methodLookup.findStatic(String.class,"valueOf",MethodType.methodType(String.class,Object.class));
                }else if(assignClass == BigDecimal.class){
                    this.instanceMethodHandle = methodLookup.findConstructor(BigDecimal.class, MethodType.methodType(void.class,String.class));
                }else if(assignClass == Short.class){
                    this.instanceMethodHandle = methodLookup.findStatic(Short.class,"valueOf", MethodType.methodType(Short.class,String.class));
                }else if(assignClass == Long.class){
                    this.instanceMethodHandle = methodLookup.findStatic(Long.class,"valueOf", MethodType.methodType(Long.class,String.class));
                }else { }
            }catch (Throwable e){
                throw new RuntimeException("Occur an error when Find string transFor MethodHandle in class : " + this.assignClass);
            }
        }

        /**
         * This method is used for Value Convert, Current support target JavaType is blow.
         * @param setterHandleParamClass Target's Fields's JavaType.
         * @param sourceObject source data.
         * @return source data convert to target data. by string construct.
         */
        public static Object tryConstructSupportTransObject(Class<?> setterHandleParamClass,Object sourceObject) {
            for (TransSupportJavaType supportJavaType : TransSupportJavaType.values()) {
                if(setterHandleParamClass.isAssignableFrom(supportJavaType.assignClass)){
                    Object transForObject = null;
                    try {
                        transForObject = supportJavaType.instanceMethodHandle.invoke(sourceObject.toString());
                    } catch (Throwable throwable) { /** Data value error,set null value. **/}
                    return transForObject;
                }
            }
            return null;
        }

        /**
         * This Method return MethodHandleEnum, used for filter MethodHandle(has filling become StringValue)
         * to SuitAble MethodHandle with Target Fields's Setter.
         * @param setterHandleParamClass Target's Fields's JavaType.
         * @return target field's string construct MethodHandle.
         */
        public static MethodHandle findStringConvertHandle(Class<?> setterHandleParamClass) {
            for (TransSupportJavaType supportJavaType : TransSupportJavaType.values()) {
                if(setterHandleParamClass.isAssignableFrom(supportJavaType.assignClass)){
                    return supportJavaType.instanceMethodHandle;
                }
            }
            return null;
        }
    }

}
